package com.googlecode.dgwt.client.dojo.util;

import java.util.AbstractList;
import java.util.AbstractMap;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.Map.Entry;

import com.google.gwt.core.client.GWT;
import com.googlecode.dgwt.client.DGWT;
import com.googlecode.dgwt.client.dojo.Dojo;
import com.googlecode.dgwt.client.dojo.core.JsArray;
import com.googlecode.dgwt.client.dojo.core.JsMap;

/**
 * An native implementation of {@link Map} with predictable iteration order to
 * increase the framework performance. <br/>
 * This type of Map has the same characteristics of {@link LinkedHashMap}.
 * 
 * @author wellsantos@wellsantos.com
 * @since 0.01
 */
abstract class StringMapBase<K,V> extends AbstractMap<K,V> {

    private transient Map<K,V> proxy;
    private transient EntrySet entrySet;
    private transient JsArray<Entry<K,V>> entries;
    private transient JsMap<?> map;
    private transient int modCount;
    private transient int size;

    
    public StringMapBase() {
        this(10);
    }

    public StringMapBase(Map<K,V> src) {
        this();
        putAll(src);
    }

    public StringMapBase(int count) {
        if (!GWT.isScript() && DGWT.isOptimizeCollections()) {
            proxy = new LinkedHashMap<K,V>(count);
            return;
        }
        map = JsMap.create().cast();
        entries = JsArray.create(count);
        size = 0;
    }
    
    private void validateKey(K key) {
        String keyStr = getNativeKey(key);
        assert (keyStr != null) : "StringMap does not allow a key = NULL";
        assert (!keyStr.equals("hasOwnProperty")) : "StringMap does not allow a key = \"hasOwnProperty\"";
    }

    private Entry<K,V> addEntry(K key, V value) {
        int index = size++;
        Entry<K,V> entry = newEntry(key, value);
        setEntryIndex(getNativeKey(key), index);
        entries.set(index, entry);
        modCount++;
        return entry;
    }
    
    private V removeEntry(Entry<K,V> entry) {
        String key = getNativeKey(entry.getKey());
        int index = getEntryIndex(key);
        V value = entry.getValue();
        entries.remove(index);
        map.remove(key);
        modCount++;
        size--;
        return value;
    }

    protected Entry<K,V> newEntry(K key, V value){
        return new MapEntry<K,V>(key, value);
    }
    
    protected Entry<K,V> getEntry(Object key){
        return entries.get(getEntryIndex(getNativeKey(key)));
    }

    protected abstract String getNativeKey(Object key);

    private native int getEntryIndex(String key) /*-{
        return (this.@com.googlecode.dgwt.client.dojo.util.StringMapBase::map[key] || 0) - 1;
    }-*/;

    private native void setEntryIndex(String key, int index) /*-{
        this.@com.googlecode.dgwt.client.dojo.util.StringMapBase::map[key] = index + 1;
    }-*/;
    
    protected boolean isProxy(){
        return proxy != null;
    }

    @Override
    public void clear() {
        if (isProxy()) {
            proxy.clear();
            return;
        }
        JsUtil.destroy(map);
        map = JsMap.create().cast();
        size = 0;
        modCount++;
    }

    @Override
    public boolean containsKey(Object key) {
        if (isProxy()) {
            return proxy.containsKey(key);
        }
        return getEntry(key) != null;
    }

    @Override
    public boolean containsValue(Object value) {
        if (isProxy()) {
            return proxy.containsValue(value);
        }
        return map.containsValue(value);
    }

    @Override
    public Set<Entry<K,V>> entrySet() {
        if (isProxy()){
            return proxy.entrySet();
        }
        if (entrySet == null) {
            entrySet = new EntrySet();
        }
        return entrySet;
    }

    @Override
    public V get(Object key) {
        if (isProxy()){
            return proxy.get(key);
        }
        if (key == null){
            return null;
        }
        Entry<K,V> entry = getEntry(key);
        return entry == null ? null : entry.getValue();
    }

    @Override
    public V put(K key, V value) {
        validateKey(key);

        if (isProxy()){
            return proxy.put(key, value);
        }        
        
        Entry<K,V> entry = getEntry(key);
        
        if (entry == null) {
            addEntry(key, value);
            return null;
        }
        
        return entry.setValue(value);        
    }

    @Override
    public void putAll(Map<? extends K, ? extends V> m) {
        for (K s : m.keySet()) {
            put(s, m.get(s));
        }
    }

    @Override
    public V remove(Object key) {
        if (isProxy()){
            return proxy.remove(key);
        }
        Entry<K,V> entry = getEntry(key);
        if (entry != null){
            return removeEntry(entry);
        }
        return null;
    }

    @Override
    public int size() {
        if (isProxy()) {
            return proxy.size();
        }
        return size;
    }    

    
    class EntrySet extends AbstractList<Entry<K, V>> implements Set<Entry<K, V>>{  
     
        @Override
        public boolean add(Entry<K, V> entry) {
            V value = entry.getValue();
            return !Dojo.isEquals(StringMapBase.this.put(entry.getKey(), value), value);
        }

        @Override
        public boolean remove(Object o) {
            if (o instanceof Entry<?,?>){
                Entry<?,?> entry = (Entry<?,?>) o;
                boolean wasSet = StringMapBase.this.containsKey(entry.getKey());
                StringMapBase.this.remove(entry.getKey());
                return wasSet;
            }
            return false;
        }

        @Override
        public boolean contains(Object key) {
            if (key instanceof Entry<?,?>){ 
                Entry<?, ?> entry = (Entry<?, ?>) key;
                if (StringMapBase.this.containsKey(entry.getKey())) {
                    return Dojo.isEquals(StringMapBase.this.get(entry.getKey()), entry.getValue());
                }
            }
            return false;
        }

        @Override
        public Iterator<Entry<K, V>> iterator() {
            return new Iterator<Entry<K, V>>() {
                
                private int index = -1;
                private int lastRet = -1;
                private int expectedModCount;
                
                public boolean hasNext() {
                    checkForComodification();
                    lastRet = -1;
                    return index < size();
                }

                public Entry<K, V> next() {
                    checkForComodification();
                    rangeCheck(index);
                    return entries.get(lastRet = index++);
                }

                public void remove() {
                    if (lastRet == -1){
                        throw new IllegalStateException();
                    }
                    checkForComodification();
                    rangeCheck(lastRet);
                    StringMapBase.this.remove(entries.get(lastRet).getKey());
                    expectedModCount++;
                    if (lastRet < index){
                        index--;
                    }
                    lastRet = -1;
                }
                
                private void checkForComodification() {
                    if (index == -1){
                        expectedModCount = modCount;
                        index = 0;
                    }
                    else if (modCount != expectedModCount){
                        throw new ConcurrentModificationException();
                    }
                }
                
            };            
        }

        private void rangeCheck(int index) {
            if (index >= size() || index < 0){
                throw new NoSuchElementException("Index: "+index+",Size: "+size());
            }
        }

        @Override
        public int size() {
            return StringMapBase.this.size();
        }
    
        @Override
        public Entry<K, V> get(int index){
            rangeCheck(index);
            return StringMapBase.this.entries.get(index);
        }

        @Override
        public void add(int index, Entry<K, V> entry) {
            if (index != size()){
                throw new UnsupportedOperationException();
            }
            add(entry);            
        }

        @Override
        public Entry<K, V> remove(int index) {
            Entry<K, V> item;
            remove(item = get(index));
            return item;
        }

    }
    
}


/**
 * A simple POJO class to store key and value.
 * 
 * @author wellsantos@wellsantos.com
 * @since 0.01
 */
class MapEntry<K, V> implements Entry<K, V> {

    private final K key;
    private V value;

    public MapEntry(K key){
        this.key = key;
    }

    public MapEntry(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }

    public V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    @Override
    public boolean equals(Object a) {
        if (a == this){
            return true;
        }
        if (a instanceof Entry<?,?>) {
            Entry<?, ?> s = (Entry<?, ?>) a;
            if (Dojo.isEquals(key, s.getKey())
                    && Dojo.isEquals(getValue(), s.getValue())) {
                return true;
            }
        }
        return false;
    }

    @Override
    public int hashCode() {
        V value = getValue();
        return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode());
    }

}
